﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using CoreEx.Common.Data;
using CoreEx.Common.Extensions;
using CoreEx.Common.Proxy;
using CoreEx.Database.Metadata;
using CoreEx.DynamicInvoker;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;
using CoreEx.Database.Query;

namespace CoreEx.Database.Linq.Implementation
{
    [Implements(typeof(IMaterializer), ServiceName = "ObjectMaterializer")]
    public class ObjectMaterializer : IMaterializer, IInitialize
    {
        private IServiceContainer _serviceContainer;        
        
        private IDictionary<PropertyInfo, PropertyInfo> _ownerPropertMap = 
            new Dictionary<PropertyInfo, PropertyInfo>();

        private readonly IDictionary<Type,IDictionary<SurrogateKey,object>> _cache = 
            new Dictionary<Type, IDictionary<SurrogateKey, object>>();


        private IList _result;

        private IDictionary<Type, Type> _typeMap = new Dictionary<Type, Type>();

        private readonly IDictionary<Type, ConstructorInfo> _constructors = new Dictionary<Type, ConstructorInfo>();


        public void Materilize(ITranslateResult translateResult)
        {
            _cache.Clear();
            var projectedType = translateResult.ProjectedType;
            _result = CreateResultList(projectedType);
            DoMaterialize(translateResult);
            translateResult.Result = _result;
        }

        private void DoMaterialize(ITranslateResult translateResult)
        {
            var dbCommand = _serviceContainer.GetService<IDbCommand>();
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();            
            var reader = dbCommand.ExecuteReader((IQueryExpression)translateResult.DbExpression);
            stopwatch.Stop();
            Console.WriteLine("");
            while (reader.Read())
            {
                ProcessTranslateResult(reader, translateResult);
            }
            reader.Close();            
            foreach (var memberResult in translateResult.MemberResults.Values)
            {
                DoMaterialize(memberResult);
            }
        }


        private IList CreateResultList(Type listType)
        {
            var genericListType = typeof (List<>).MakeGenericType(listType);
            return (IList)Activator.CreateInstance(genericListType);
        }


        private void ProcessTranslateResult(IDataRecord dataRecord, ITranslateResult translateResult)
        {                        
                var projectionTarget = translateResult.ProjectionTargets.First().Value;
                var instance = ProcessProjectionTarget(dataRecord, projectionTarget);                
                if (instance == null)
                    return;
                if (translateResult.RelationLoadTarget != null)
                {
                    var property = translateResult.RelationLoadTarget.TargetProperty;
                    var foreignKeyColumns =
                        translateResult.RelationLoadTarget.PropertyMapping.ForeignKeyInfo.Columns.Select(
                            c => c.ForeignColumnInfo.ColumnName).ToArray();

                    var foreignKeyValues = GetKeyValues(dataRecord, foreignKeyColumns);
                    var foreignSurrogateKey = new SurrogateKey(foreignKeyValues);
                    var parentCache = _cache[property.DeclaringType];
                    var parentInstance = parentCache[foreignSurrogateKey];
                    var list = (IList)property.DynamicInvoke(parentInstance);
                    list.Add(instance);

                    var ownerProperty = GetOwnerProperty(property);
                    if (ownerProperty != null)
                    {
                        ownerProperty.DynamicInvoke(instance, parentInstance);
                    }                    
                }
                else
                {
                    _result.Add(instance);
                }
        }

        private PropertyInfo GetOwnerProperty(PropertyInfo propertyInfo)
        {
            
            if (!_ownerPropertMap.ContainsKey(propertyInfo))
            {
                var targetType = propertyInfo.PropertyType.GetGenericElementType();
                var ownerProperty = targetType.GetProperties().Where(p => p.PropertyType == propertyInfo.DeclaringType).FirstOrDefault();
                _ownerPropertMap.Add(propertyInfo,ownerProperty);
            }

            return _ownerPropertMap[propertyInfo];            

        }



        private object ProcessProjectionTarget(IDataRecord dataRecord,IProjectionTarget projectionTarget)
        {
            var targetType = projectionTarget.GetMemberType();
            
            SurrogateKey surrogateKey = CreatePrimarySurrogateKey(dataRecord, projectionTarget);  
            if (surrogateKey == null)
                return null;

            if (IsCached(surrogateKey,targetType))
                return _cache[targetType][surrogateKey];
            
            var instance = CreateInstance(targetType);
            foreach (var map in projectionTarget.MemberMap)
            {
                var value = dataRecord[map.Value];                
                if (value == DBNull.Value)
                    value = null;
                ((PropertyInfo) map.Key).DynamicInvoke(instance, value);
            }
       
            AddToCache(surrogateKey,instance,targetType);

            foreach (var child in projectionTarget.Children.Where(c => c.Projection != null))
            {
                var childInstance = ProcessProjectionTarget(dataRecord, child);
                ((PropertyInfo) child.Member).DynamicInvoke(instance, childInstance);
                var propertyMappingInfo = projectionTarget.TypeMappingInfo.ColumnMappings[(PropertyInfo) child.Member];
                if (propertyMappingInfo.RelationType == CardinalityType.OneToOne)
                {
                    var ownerProperty = GetOwnerProperty((PropertyInfo) child.Member);
                    if (ownerProperty != null)
                        ownerProperty.DynamicInvoke(childInstance, instance);
                }
            }

            return instance;
        }
        

        private bool IsCached(SurrogateKey hashCode, Type targetType)
        {
            if (!_cache.ContainsKey(targetType))
                return false;
            if (!_cache[targetType].ContainsKey(hashCode))
                return false;
            return true;
        }


        private void AddToCache(SurrogateKey surrogateKey, object instance, Type type)
        {        
            if (!_cache.ContainsKey(type))
                _cache.Add(type,new Dictionary<SurrogateKey, object>());
            _cache[type].Add(surrogateKey,instance);
        }


        private SurrogateKey CreatePrimarySurrogateKey(IDataRecord dataRecord, IProjectionTarget projectionTarget)
        {
            var primaryKeyColumns = projectionTarget.PrimaryKeyColumnNames;
            object[] values = GetKeyValues(dataRecord, primaryKeyColumns);
            if (values == null)
                return null;
            var surrogateKey = new SurrogateKey(values);
            return surrogateKey;
        }


       
        private object[] GetKeyValues(IDataRecord dataRecord, string[] columnNames)
        {
            var values = new object[columnNames.Length];
            for (int i = 0; i < columnNames.Length; i++)
            {
                var columnName = columnNames[i];
                var value = dataRecord[columnName];
                if (value == DBNull.Value)
                    return null;
                values[i] = value;
            }
            return values;
        }
                

        private object CreateInstance(Type type)
        {            
            object instance;
            if (!_constructors.ContainsKey(type))
            {
                instance =  _serviceContainer.GetService(type);
                var concreteType = instance.GetType();
                var constructorInfo = concreteType.GetConstructor(Type.EmptyTypes);
                _constructors.Add(type,constructorInfo);                                    
            }
            else
            {
                var constructorInfo = _constructors[type];
                instance = constructorInfo.DynamicInvoke();                
            }

            return instance;
        
        }




        public void Initialize(IServiceContainer source)
        {
            _serviceContainer = source;
        }
    }
}
